package no.group.gowther.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import no.group.gowther.dao.OnePieceDao;
import no.group.gowther.enums.OnePieceType;
import no.group.gowther.exception.*;
import no.group.gowther.service.dto.*;
import no.group.gowther.service.dto.param.OnePieceQueryDTO;
import no.group.gowther.service.dto.param.OnePieceSaveDTO;
import no.group.gowther.service.dto.param.OnePieceUpdateDTO;
import no.group.gowther.dao.entity.OnePiece;
import no.group.gowther.dao.entity.OnePieceTag;
import no.group.gowther.dao.entity.param.OnePieceQueryParam;
import no.group.gowther.util.FileUtils;
import no.group.gowther.util.FilePathGenerator;
import no.group.gowther.util.OnePieceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author ayatsuji
 * @date 2022-04-26 23:14
 * @since
 */
@Service
public class OnePieceServiceImpl extends ServiceImpl<OnePieceDao, OnePiece> implements OnePieceService {
    private static final Logger LOGGER = LoggerFactory.getLogger(OnePieceServiceImpl.class);

    @Autowired
    private OnePieceTagService onePieceTagService;
    @Autowired
    private OnePieceFilingService onePieceFilingService;
    @Autowired
    private TagService tagService;
    @Autowired
    private ImageService imageService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long create(OnePieceSaveDTO onePieceSaveDTO) {
        final OnePieceType onePieceType = OnePieceUtils.resolveOnePieceType(onePieceSaveDTO.getFilePath());
        final OnePieceFilingService.FilingExecutor filingExecutor =
                onePieceFilingService.generateFilingExecutor(onePieceType, onePieceSaveDTO.getName());

        OnePiece onePiece = new OnePiece();
        onePiece.setType(onePieceType);
        onePiece.setName(filingExecutor.getFilename());
        onePiece.setIndex(filingExecutor.getSliceIndex());
        onePiece.setCreatedAt(Timestamp.valueOf(LocalDateTime.now()));
        save(onePiece);
        if (!CollectionUtils.isEmpty(onePieceSaveDTO.getTagId())) {
            final List<OnePieceTag> onePieceTags = onePieceSaveDTO.getTagId().stream().map(tagId -> new OnePieceTag(onePiece.getId(), tagId)).collect(Collectors.toList());
            onePieceTagService.saveBatch(onePieceTags);
        }

        try {
            filingExecutor.filing(onePieceSaveDTO.getFilePath());
        } catch (OnePieceFilingException e) {
            LOGGER.error("归档onePiece失败", e);
            throw BusinessException.createException(ErrorMessage.SYSTEM_ERROR.getErrorCode(), "归档onePiece失败");
        }
        return onePiece.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(OnePieceUpdateDTO onePieceUpdateDTO) {
        OnePiece onePiece = getById(onePieceUpdateDTO.getId());
        if (onePiece == null) {
            throw BusinessException.createException(ErrorMessage.PARAM_ERROR.getErrorCode(), "onePiece不存在");
        }
        if (!Objects.equals(onePiece.getName(), onePieceUpdateDTO.getName())) {
            Path originalPath = onePieceFilingService.generateFullStorePath(onePiece);
            onePiece.setName(onePieceUpdateDTO.getName());
            Path newPath = new FilePathGenerator(onePieceFilingService.generateFullStorePath(onePiece)).generate();
            onePiece.setName(newPath.getFileName().toString());

            try {
                Files.move(originalPath, newPath);
            } catch (IOException e) {
                LOGGER.error("更新onepiece，重命名文件失败", e);
            }
        }
        onePiece.setUpdatedAt(Timestamp.valueOf(LocalDateTime.now()));
        updateById(onePiece);

        final List<OnePieceTag> onePieceTags = onePieceTagService.list(
                new LambdaQueryWrapper<OnePieceTag>().eq(OnePieceTag::getOnePieceId, onePiece.getId())
        );
        final List<Long> currentTags = onePieceTags.stream()
                .map(OnePieceTag::getTagId)
                .collect(Collectors.toList());
        final List<OnePieceTag> newTags = onePieceUpdateDTO.getTagId().stream()
                .filter(tag -> !currentTags.contains(tag))
                .map(tagId -> new OnePieceTag(onePiece.getId(), tagId))
                .collect(Collectors.toList());
        final List<Long> removeTagId = onePieceTags.stream()
                .filter(onePieceTag -> !onePieceUpdateDTO.getTagId().contains(onePieceTag.getTagId()))
                .map(OnePieceTag::getId)
                .collect(Collectors.toList());
        onePieceTagService.saveBatch(newTags);
        onePieceTagService.removeBatchByIds(removeTagId);
    }

    @Override
    public ListDTO<OnePieceListDTO> queryList(OnePieceQueryDTO onePieceQueryDTO) {
        List<OnePiece> onePieces;
        if (onePieceQueryDTO.getType().isEmpty()) {
            onePieces = Collections.emptyList();
        } else if (!onePieceQueryDTO.getTagId().isEmpty()) {
            onePieces = intersectTagQuery(onePieceQueryDTO);
        } else {
            onePieces = queryList(onePieceQueryDTO.getTypeString(), onePieceQueryDTO.getTagId());
        }
        final List<OnePieceListDTO> pieceDTOS = onePieces.stream()
                .skip((long) (onePieceQueryDTO.getPageNum() - 1) * onePieceQueryDTO.getPageSize())
                .limit(onePieceQueryDTO.getPageSize())
                .map(this::convertList)
                .collect(Collectors.toList());
        return new ListDTO<>(onePieces.size(), pieceDTOS);
    }

    private List<OnePiece> intersectTagQuery(OnePieceQueryDTO onePieceQueryDTO) {
        List<Long> queryTag = onePieceQueryDTO.getTagId();
        if (queryTag.isEmpty()) {
            return Collections.emptyList();
        }
        Set<OnePiece> intersectResult = null;
        for (Long tagId: queryTag) {
            final List<OnePiece> result = queryList(onePieceQueryDTO.getTypeString(), tagService.queryDescendantTag(Collections.singletonList(tagId)));
            if (intersectResult == null) {
                intersectResult = new HashSet<>(result);
                continue;
            }
            intersectResult.retainAll(result);
            if (intersectResult.isEmpty()) break;
        }
        return new ArrayList<>(intersectResult);
    }

    private List<OnePiece> queryList(List<String> types, List<Long> tagIds) {
        OnePieceQueryParam onePieceQueryParam = new OnePieceQueryParam(types, tagIds);
        return getBaseMapper().query(onePieceQueryParam);
    }

    private OnePieceListDTO convertList(OnePiece onePiece) {
        OnePieceListDTO dto = new OnePieceListDTO();
        dto.setId(onePiece.getId());
        dto.setName(onePiece.getName());
        dto.setType(onePiece.getType());
        if (onePiece.getType() == OnePieceType.PIC || onePiece.getType() == OnePieceType.ALBUM) {
            List<ImageCode> imageCodes = generateUrl(onePiece);
            dto.setImageCode(imageCodes.get(0));
            dto.setContainNum(imageCodes.size());
        }
        return dto;
    }

    private List<ImageCode> generateUrl(OnePiece onePiece) {
        final Path fullStorePath = onePieceFilingService.generateFullStorePath(onePiece);
        return switch (onePiece.getType()) {
            case PIC -> Collections.singletonList(imageService.generateImageCode(fullStorePath));
            case ALBUM -> imageService.generateDirImageCode(fullStorePath);
            case ANIME -> Collections.emptyList();
        };
    }

    @Override
    public OnePieceDTO queryDetail(Long id) {
        final OnePiece onePiece = getById(id);
        if (onePiece == null) {
            throw BusinessException.createException(ErrorMessage.PARAM_ERROR.getErrorCode(), "one_piece_id=[%s]不存在".formatted(id));
        }
        final List<TagDTO> tagDTOS = onePieceTagService.queryTag(id);
        return convert(onePiece, tagDTOS);
    }

    private OnePieceDTO convert(OnePiece onePiece, List<TagDTO> tagDTOS) {
        OnePieceDTO dto = new OnePieceDTO();
        dto.setId(onePiece.getId());
        dto.setName(onePiece.getName());
        dto.setType(onePiece.getType());
        dto.setImageCodes(generateUrl(onePiece));
        dto.setTag(tagDTOS);
        return dto;
    }

    @Service
    public static class OnePieceFilingService {
        // 各种类型文件的当前归档分片目录路径，归档文件数量达到sliceSize，会创建下一个子目录并切换该对应路径
        private Map<OnePieceType, Path> index;

        @Autowired
        private OnePieceConfig onePieceConfig;

        @PostConstruct
        public void init() throws IOException, CalOnePieceIndexException {
            initDir();
            initIndex();
        }

        // 创建各类型文件的归档目录以及分片目录
        private void initDir() throws IOException {
            for (OnePieceType onePieceType: OnePieceType.values()) {
                final Path typeDirPath = Paths.get(onePieceConfig.onePieceHome, onePieceType.name());
                if (!Files.exists(typeDirPath)) {
                    FileUtils.createDir(typeDirPath);
                    FileUtils.createDir(Paths.get(typeDirPath.toString(), generateSliceDirName(1)));
                }
            }
        }

        // 初始化各种类型文件的当前归档分片目录路径
        private void initIndex() throws CalOnePieceIndexException {
            index = new HashMap<>();
            for (OnePieceType onePieceType: OnePieceType.values()) {
                calNextIndex(onePieceType);
            }
        }

        // 根据分片下标计算分片目录名称，即文件的范围
        private String generateSliceDirName(int sliceIndex) {
            if (sliceIndex <= 0) throw new IllegalArgumentException("sliceIndex需要大于0，当前为" + sliceIndex);
            final int start = ((sliceIndex - 1) * onePieceConfig.sliceSize) + 1;
            return start + "-" + (start + onePieceConfig.sliceSize - 1);
        }

        public FilingExecutor generateFilingExecutor(OnePieceType onePieceType, String filename) {
            final Path targetDir = index.get(onePieceType);
            final Path filingPath = new FilePathGenerator(Paths.get(targetDir.toString(), filename)).generate();
            final String sliceIndex = extractSliceIndex(targetDir.toString());
            return new FilingExecutor(filingPath.getFileName().toString(), sliceIndex, onePieceType);
        }

        private String extractSliceIndex(String filingPath) {
            return filingPath.substring(filingPath.lastIndexOf(File.separator) + 1);
        }

        private void filing(Path file, Path targetFile, OnePieceType onePieceType) throws OnePieceFilingException {
            try {
                Files.move(file, targetFile);
                calNextIndex(onePieceType);
            } catch (CalOnePieceIndexException | IOException e) {
                throw new OnePieceFilingException("归档onePiece失败，file=[%s]".formatted(file), e);
            }
        }

        public Path generateFullStorePath(OnePiece onePiece) {
            return Paths.get(onePieceConfig.onePieceHome, onePiece.getType().name(), onePiece.getIndex(), onePiece.getName());
        }

        private void calNextIndex(OnePieceType onePieceType) throws CalOnePieceIndexException {
            final Path typeDirPath = Paths.get(onePieceConfig.onePieceHome, onePieceType.name());
            try {
                Path latest = getLatestTypeDir(typeDirPath);
                if (FileUtils.fileCount(latest) >= onePieceConfig.sliceSize) {
                    int nextIndex = (int) FileUtils.fileCount(typeDirPath) + 1;
                    latest = Paths.get(typeDirPath.toString(), generateSliceDirName(nextIndex));
                    FileUtils.createDir(latest);
                }
                index.put(onePieceType, latest);
            } catch (IOException e) {
                throw new CalOnePieceIndexException("计算类型[%s]index失败".formatted(onePieceType), e);
            }
        }

        private Path getLatestTypeDir(Path typeDirPath) throws IOException {
            return Files.list(typeDirPath).sorted(Comparator.reverseOrder())
                    .limit(1).toList().get(0);
        }

        public class FilingExecutor {
            private final String filename;
            private final String sliceIndex;
            private final OnePieceType type;
            private final Path targetFile;

            public FilingExecutor(String filename, String sliceIndex, OnePieceType type) {
                this.filename = filename;
                this.sliceIndex = sliceIndex;
                this.type = type;
                this.targetFile = Paths.get(onePieceConfig.onePieceHome, type.name(), sliceIndex, filename);
            }

            public void filing(Path file) throws OnePieceFilingException {
                OnePieceFilingService.this.filing(file, targetFile, type);
            }

            public String getFilename() {
                return filename;
            }

            public String getSliceIndex() {
                return sliceIndex;
            }
        }
    }
}
